﻿using System;
using System.Collections.Specialized;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.Web.UI.WebControls;
using TallComponents.Web.Pdf.Services;
using TallComponents.Web.Storage;
using System.Drawing.Drawing2D;
using TallComponents.PDF.Rasterizer.Configuration;
using TallComponents.PDF.Rasterizer.Diagnostics;

namespace TallComponents.Web.Pdf
{
   /// <summary>
   /// HttpHandler used to render (partial) PDF pages.
   /// </summary>
   public class PdfToImageHandler : IHttpHandler, IReadOnlySessionState
   {
      /// <summary>
      /// Initializes a new instance of the <see cref="PdfToImageHandler"/> class.
      /// </summary>
      public PdfToImageHandler()
         : this(PdfStorage.Provider, new TraceSourceLogger())
      {
      }

      /// <summary>
      /// Initializes a new instance of the <see cref="PdfToImageHandler"/> class.
      /// </summary>
      /// <param name="storage">The PDF storage provider to use.</param>
      public PdfToImageHandler(IPdfStorage storage)
         : this(storage, new TraceSourceLogger())
      {
      }

      /// <summary>
      /// Initializes a new instance of the <see cref="PdfToImageHandler"/> class.
      /// </summary>
      /// <param name="storage">The PDF storage provider to use.</param>
      /// <param name="logger">The logging service.</param>
      internal PdfToImageHandler(IPdfStorage storage, ILog logger)
      {
         if (null == storage)
            throw new ArgumentNullException("storage");

         _storage = storage;
         _logger = logger;
      }

      /// <summary>
      /// Gets the URL needed to generate a thumbnail of a PDF document.
      /// </summary>
      /// <param name="baseUrl">A URL associated with a <see cref="PdfToImageHandler"/> handler.</param>
      /// <param name="storageKey">The storage key.</param>
      /// <param name="scale">The scale as a fraction; 0.1 equals a scale of 10%. Values &lt;= 0 and &gt;0.25 are ignored.</param>
      /// <param name="pageIndex">The 0-based index of the page.</param>
      /// <returns>
      /// A URL that will generate a thumbnail for the specified page.
      /// </returns>
      public static string GetThumbnailUrl(string baseUrl, string storageKey, double scale, int pageIndex)
      {
         return GetThumbnailUrl(baseUrl, storageKey, scale, pageIndex, 0);
      }

      /// <summary>
      /// Gets the URL needed to generate a thumbnail of a PDF document.
      /// </summary>
      /// <param name="baseUrl">A URL associated with a <see cref="PdfToImageHandler"/> handler.</param>
      /// <param name="storageKey">The storage key.</param>
      /// <param name="scale">The scale as a fraction; 0.1 equals a scale of 10%. Values &lt;= 0 and &gt;0.25 are ignored.</param>
      /// <param name="pageIndex">The 0-based index of the page.</param>
      /// <param name="rotation">The clock-wise rotation if the page in degrees. Acceptable values are 0, 90, 180 and 270. </param>
      /// <returns>
      /// A URL that will generate a thumbnail for the specified page.
      /// </returns>
      public static string GetThumbnailUrl(string baseUrl, string storageKey, double scale, int pageIndex, int rotation)
      {
         return string.Format(CultureInfo.InvariantCulture, GetThumbnailTemplateUrl(baseUrl), storageKey, scale.ToString("0.00", CultureInfo.InvariantCulture), rotation, pageIndex);
      }

      internal static string GetThumbnailTemplateUrl(string baseUrl)
      {
         var sb = new StringBuilder(baseUrl);
         sb.Append("?");
         sb.AppendFormat("{0}=", RequestParameters.UNIQUEKEY);
         sb.Append("{0}");
         sb.AppendFormat("&{0}=", RequestParameters.SCALEKEY);
         sb.Append("{1}");
         sb.AppendFormat("&{0}=", RequestParameters.ROTATIONKEY);
         sb.Append("{2}");
         sb.AppendFormat("&{0}=", RequestParameters.FIRSTPAGEINDEXKEY);
         sb.Append("{3}");
         sb.Append("{4}");
sb.AppendFormat("&{0}=", RequestParameters.MODIFICATIONDATE);
sb.Append("{5}");
         return sb.ToString();
      }

      /// <summary>
      /// Gets the URL needed to generate a preview of a PDF document.
      /// </summary>
      /// <param name="baseUrl">A URL associated with a <see cref="PdfToImageHandler"/> instance.</param>
      /// <param name="storageKey">The storage key.</param>
      /// <param name="width">The width of the clipping rectangle.</param>
      /// <param name="height">The height of the clipping rectangle.</param>
      /// <param name="scale">The scale as a fraction; 1 equals a scale of 100%.</param>
      /// <param name="pageIndex">The 0-based index of the page.</param>
      /// <param name="dx">The horizontal offset of the clipping rectangle.</param>
      /// <param name="dy">The vertical offset of the clipping rectangle.</param>
      /// <returns>A URL that will generate a preview for the specified page.</returns>
      public static string GetPreviewUrl(string baseUrl, string storageKey, Unit width, Unit height, double scale, int pageIndex, int dx, int dy)
      {
         return GetPreviewUrl(baseUrl, storageKey, width, height, scale, pageIndex, dx, dy, 0);
      }

      /// <summary>
      /// Gets the URL needed to generate a preview of a PDF document.
      /// </summary>
      /// <param name="baseUrl">A URL associated with a <see cref="PdfToImageHandler"/> instance.</param>
      /// <param name="storageKey">The storage key.</param>
      /// <param name="width">The width of the clipping rectangle.</param>
      /// <param name="height">The height of the clipping rectangle.</param>
      /// <param name="scale">The scale as a fraction; 1 equals a scale of 100%.</param>
      /// <param name="pageIndex">The 0-based index of the page.</param>
      /// <param name="dx">The horizontal offset of the clipping rectangle.</param>
      /// <param name="dy">The vertical offset of the clipping rectangle.</param>
      /// <param name="rotation"></param>
      /// <returns>A URL that will generate a preview for the specified page.</returns>
      public static string GetPreviewUrl(string baseUrl, string storageKey, Unit width, Unit height, double scale, int pageIndex, int dx, int dy, int rotation)
      {
         return GetPreviewUrl(baseUrl, storageKey, width, height, scale, pageIndex, dx, dy, 0, 1, 0, PageLayout.Single);
      }

      /// <summary>
      /// Gets the URL needed to generate a preview of a PDF document.
      /// </summary>
      /// <param name="baseUrl">A URL associated with a <see cref="PdfToImageHandler"/> instance.</param>
      /// <param name="storageKey">The storage key.</param>
      /// <param name="width">The width of the clipping rectangle.</param>
      /// <param name="height">The height of the clipping rectangle.</param>
      /// <param name="scale">The scale as a fraction; 1 equals a scale of 100%.</param>
      /// <param name="pageIndex">The 0-based index of the page.</param>
      /// <param name="dx">The horizontal offset of the clipping rectangle.</param>
      /// <param name="dy">The vertical offset of the clipping rectangle.</param>
      /// <param name="maxVerticalPages">The maximum number of pages to show below each other.</param>
      /// <param name="verticalGap">The vertical space in pixels between 2 page.</param>
      /// /// <param name="layout">The page layout mode.</param>
      /// <param name="rotation"></param>
      /// <returns>A URL that will generate a preview for the specified page.</returns>
      public static string GetPreviewUrl(string baseUrl, string storageKey, Unit width, Unit height, double scale, int pageIndex, int dx, int dy, int rotation, int maxVerticalPages, int verticalGap, PageLayout layout)
      {
         var sb = new StringBuilder(baseUrl);
         sb.Append("?");

         sb.AppendFormat("{0}={1}", RequestParameters.UNIQUEKEY, storageKey);
         sb.AppendFormat("&{0}={1}", RequestParameters.FIRSTPAGEINDEXKEY, pageIndex);
         sb.AppendFormat(CultureInfo.InvariantCulture, "&{0}={1:0.00}", RequestParameters.SCALEKEY, scale);
         sb.AppendFormat("&{0}={1}", RequestParameters.WIDTHKEY, width.Value);
         sb.AppendFormat("&{0}={1}", RequestParameters.HEIGHTKEY, height.Value);
         sb.AppendFormat("&{0}={1}", RequestParameters.DXKEY, dx);
         sb.AppendFormat("&{0}={1}", RequestParameters.DYKEY, dy);
         sb.AppendFormat("&{0}={1}", RequestParameters.ROTATIONKEY, RequestParameters.NormalizeRotation(rotation));
         sb.AppendFormat("&{0}={1}", RequestParameters.ROTATIONKEY, RequestParameters.NormalizeRotation(rotation));
         sb.AppendFormat("&{0}={1}", RequestParameters.VERTICALGAPKEY, verticalGap);
         sb.AppendFormat("&{0}={1}", RequestParameters.PAGELAYOUTKEY, (int)layout);

         return sb.ToString();
      }

      /// <summary>
      /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
      /// </summary>
      /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references 
      /// to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
      void IHttpHandler.ProcessRequest(HttpContext context)
      {
         try
         {
            if (String.IsNullOrEmpty(context.Request.QueryString[RequestParameters.UNIQUEKEY]))
            {
               context.Response.Close();
            }

            if (context.Request.QueryString[RequestParameters.IMAGETYPE] == "viewer")
            {

               //System.Threading.Thread.Sleep(5000);

               var requestParams = new PreviewRequesParameters(context.Request.QueryString);

               _logger.Info(string.Format("Getting the '{0}' storage provider", String.IsNullOrEmpty(requestParams.StorageProviderName) ? "default": requestParams.StorageProviderName));

               IPdfStorage storage = String.IsNullOrEmpty(requestParams.StorageProviderName)
                                         ? PdfStorage.Provider
                                         : PdfStorage.Providers[requestParams.StorageProviderName];

               _logger.Info(string.Format("Rendering document '{0}'\nStorage key:{1}\nPages: {2}..{3}",
                   storage.GetDocument(requestParams.Key).FileName,
                   requestParams.Key,
                   requestParams.FirstPageIndex,
                   requestParams.LastPageIndex));

               var settings = new RenderSettings();
               ConfigureTextSettings(settings, context);

               var generator = new PlainPreviewGenerator();
               
               byte[] preview = generator.GeneratePreview(context, requestParams, storage, _logger, settings);

               if (!context.Response.IsClientConnected && preview != null)
                  return;
#if NOCACHE
               context.Response.Cache.SetNoServerCaching();
               context.Response.Cache.SetNoStore(); // use this for testing only
#else
               context.Response.Cache.VaryByParams[RequestParameters.UNIQUEKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.FIRSTPAGEDXKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.FIRSTPAGEDYKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.WIDTHKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.SCALEKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.HEIGHTKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.ROTATIONKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.LAYOUTINSTRUCTIONSKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.FIRSTPAGEINDEXKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.LASTPAGEINDEXKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.PAGELAYOUTKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.VERTICALGAPKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.HORISONTALGAPKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.STORAGEPROVIDERNAME] = true;

               context.Response.Cache.VaryByParams[RequestParameters.DXKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.DYKEY] = true;
               context.Response.Cache.VaryByParams[RequestParameters.MODIFICATIONDATE] = true;
               //context.Response.Cache.VaryByParams[ QUALITYKEY ] = true;
               context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(30));
               context.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
               context.Response.Cache.SetValidUntilExpires(true);
#endif
               context.Response.ContentType = "image/png";
               context.Response.OutputStream.Write(preview, 0, preview.Length);

            }
            else if (context.Request.QueryString[RequestParameters.IMAGETYPE] == "print")
            {
               var requestParams = new PreviewRequesParameters(context.Request.QueryString);
               _logger.Info(string.Format("Rendering document {0} for printing, page {1}",
                   requestParams.Key,
                   requestParams.FirstPageIndex));

               var settings = new RenderSettings();
               ConfigureTextSettings(settings, context);

               var generator = new PrintPreviewGenerator();

               IPdfStorage storage = String.IsNullOrEmpty(requestParams.StorageProviderName)
                                         ? PdfStorage.Provider
                                         : PdfStorage.Providers[requestParams.StorageProviderName];

               byte[] preview = generator.GeneratePreview(context, requestParams, storage, _logger, settings);

               if (!context.Response.IsClientConnected && preview != null)
                  return;

               context.Response.ContentType = "image/png";
               context.Response.OutputStream.Write(preview, 0, preview.Length);
            }
            else
            {
               var requestParams = new ThumbnailRequestParameters(context.Request.QueryString);

               _logger.Info(string.Format("Generating thumbnail for document {0} page {1}", requestParams.Key,
                                            requestParams.FirstPageIndex));

               IPdfStorage storage = String.IsNullOrEmpty(requestParams.StorageProviderName)
                                          ? PdfStorage.Provider
                                          : PdfStorage.Providers[requestParams.StorageProviderName];

               GenerateThumbnail(context, requestParams, storage, _logger);
            }
            _logger.Info("Done");
         }
         catch (Exception ex)
         {
            _logger.Fatal("Unhandled exception. Please contact TallComponents support.", ex);
            throw;
         }
         finally
         {
            _logger.Flush();
         }
      }

      private static void ConfigureTextSettings(RenderSettings settings, HttpContext context)
      {
         if (settings != null && context != null)
         {
            try
            {
               var fontsubstitutionsxml = context.Server.MapPath("~/fontsubstitutions.xml");
               if (File.Exists(fontsubstitutionsxml))
               {
                  using (var fs = new FileStream(fontsubstitutionsxml, FileMode.Open, FileAccess.Read))
                  {
                     settings.TextSettings.FontSubstitutionMap.Load(fs);
                  }
               }
            }
            catch (Exception)
            {
               // consume exception; that would cause pages not to render
               System.Diagnostics.Debug.WriteLine("Failed to load ~/fontsubstitutions.xml.");
            }

            settings.TextSettings.FontSearchPath = context.Server.MapPath("~/fonts") + ";" +
                                                   settings.TextSettings.FontSearchPath;
            TextRenderSettings.CMapFolder = context.Server.MapPath("~/cmaps");
         }
      }

      private static ImageCodecInfo GetEncoderInfo(String mimeType)
      {
         foreach (var encoder in ImageCodecInfo.GetImageEncoders())
         {
            if (encoder.MimeType == mimeType)
               return encoder;
         }
         return null;
      }

      private static void GenerateThumbnail(HttpContext context, ThumbnailRequestParameters request, IPdfStorage storage, ILog logger)
      {
         // default page size is A4
         var pageWidth = (int)(210 / 25.4 * 72); // A4 -> 210 mm / 25.4 mm/inch * 72 ppi
         var pageHeight = (int)(297.5 / 25.4 * 72); // A4 -> 297.5 mm / 25.4 mm/inch * 72 ppi

         var page = storage.GetPage(request.Key, request.FirstPageIndex);

         if (null != page)
         {
            pageWidth = (request.Rotation % 180 == 0) ? (int)page.Width : (int)page.Height;
            pageHeight = (request.Rotation % 180 == 0) ? (int)page.Height : (int)page.Width;
         }

         if (context.Response.IsClientConnected)
         {
            using (var intermediateBitmap = new Bitmap(pageWidth, pageHeight))
            {
               var graphics = Graphics.FromImage(intermediateBitmap);
               switch (request.Rotation)
               {
                  case 0:
                     break;
                  case 90:
                     graphics.RotateTransform(request.Rotation);
                     graphics.TranslateTransform(0, (float)(-1 * page.Height));
                     break;
                  case 180:
                     graphics.RotateTransform(request.Rotation);
                     graphics.TranslateTransform((float)(-1 * page.Width), (float)(-1 * page.Height));
                     break;
                  case 270:
                     graphics.RotateTransform(request.Rotation);
                     graphics.TranslateTransform((float)(-1 * page.Width), 0);
                     break;
               }
               graphics.Clear(Color.White);
               if (null != page)
               {
                  var settings = new RenderSettings();
                  ConfigureTextSettings(settings, context);
                  settings.ColorSettings.TransformationMode = ColorTransformationMode.HighSpeed;
                  var summary = new Summary();
                  try
                  {
                     page.Draw(graphics, settings, summary);
                  }
                  finally
                  {
                     foreach (var message in summary.Messages)
                     {
                        switch (message.Severity)
                        {
                           case MessageSeverity.Information:
                              logger.Info(message.Text);
                              break;
                           case MessageSeverity.Warning:
                              logger.Warn(message.Text);
                              break;
                           case MessageSeverity.Error:
                              logger.Error(message.Text);
                              break;
                        }
                     }
                  }
               }

               float zoom = request.ZoomPercentage;

               if (zoom <= 0 || zoom > 0.25)
               {
                  zoom = 0.25F;
               }

               var thumbnailWidth = (int)(intermediateBitmap.Width * zoom);
               var thumbnailHeight = (int)(intermediateBitmap.Height * zoom);
               using (var image = intermediateBitmap.GetThumbnailImage(thumbnailWidth, thumbnailHeight, null, new IntPtr()))
               {
#if NOCACHE
                  context.Response.Cache.SetNoServerCaching();
                  context.Response.Cache.SetNoStore(); // use this for testing only
#else
                  context.Response.Cache.VaryByParams[RequestParameters.UNIQUEKEY] = true;
                  context.Response.Cache.VaryByParams[RequestParameters.FIRSTPAGEINDEXKEY] = true;
                  context.Response.Cache.VaryByParams[RequestParameters.SCALEKEY] = true;
                  context.Response.Cache.VaryByParams[RequestParameters.PAGELAYOUTKEY] = true;
                  //context.Response.Cache.VaryByParams[ QUALITYKEY ] = true;
                  context.Response.Cache.VaryByParams[RequestParameters.ROTATIONKEY] = true;
                  context.Response.Cache.VaryByParams[RequestParameters.MODIFICATIONDATE] = true;
                  context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(30));
                  context.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
                  context.Response.Cache.SetValidUntilExpires(true);
#endif
                  context.Response.ContentType = "image/png";

                  using (var buffer = new MemoryStream())
                  {
                     image.Save(buffer, ImageFormat.Png);

                     byte[] data = buffer.ToArray();
                     context.Response.OutputStream.Write(data, 0, data.Length);
                  }

               }
            }
         }
      }

      /// <summary>
      /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
      /// </summary>
      /// <value></value>
      /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
      /// </returns>
      public bool IsReusable
      {
         get { return false; }
      }

      private IPdfStorage _storage;
      private readonly ILog _logger;
   }
}
